---
title: 中间件
description: 了解如何在 Astro 中使用中间件
i18nReady: true
---
import PackageManagerTabs from '~/components/tabs/PackageManagerTabs.astro';

**中间件**允许你拦截请求和响应，并在即将渲染页面或端点时动态注入行为。对于所有预渲染的页面，这种渲染发生在构建时，但对于按需渲染的页面，这种渲染发生在请求路由时。

这也允许你通过修改在所有 Astro 组件和 API 端点中可用的 `locals` 对象，设置和共享跨端点和页面的请求特定信息。即使在构建时运行这个中间件时，这个对象也是可用的。

## 基本用法

1. 创建 `src/middleware.js|ts`（或者，你也可以创建 `src/middleware/index.js|ts`）

2. 在这个文件中，导出一个接收 [`context` 对象](#context-对象) 的 [`onRequest()`](/zh-cn/reference/api-reference/#onrequest) 函数。注意这里不能是默认导出。

    ```js title="src/middleware.js"
    export function onRequest ({ locals, request }, next) {
        // 拦截一个请求里的数据
        // 可选地修改 `locals` 中的属性
        locals.title = "New title";

        // 返回一个 Response 或者调用 `next()` 的结果
        return next();
    };
    ```

3. 在任何 `.astro` 文件中，使用 `Astro.locals` 访问响应数据。

    ```astro title="src/components/Component.astro"
    ---
    const data = Astro.locals;
    ---
    <h1>{data.title}</h1>
    <p>这个 {data.property} 来自中间件。</p>
    ```
### `context` 对象

[`context`](/zh-cn/reference/api-reference/#端点上下文) 对象包含需要对其他中间件、API 路由和 `.astro` 路由在渲染过程中可用的信息。

它是一个传入 `onRequest()` 的可选参数，可能包含 `locals` 对象以及其他在渲染期间共享的任何属性。例如，`context` 对象可能包含用于认证的 cookies。

### 在 `context.locals` 中存储数据

`context.locals` 是一个可以在中间件中操作、包含来自 `Response` 的数据的对象。

这个 `locals` 对象在请求处理过程中被传递，并作为 [`APIContext`](/zh-cn/reference/api-reference/#端点上下文) 和 [`AstroGlobal`](/zh-cn/reference/api-reference/#astrolocals) 的属性可用。这使得中间件，API 路由和 `.astro` 页面之间可以共享数据。这对于在渲染步骤中存储请求特定的数据（例如用户数据）很有用。

:::tip[集成属性]
[集成](/zh-cn/guides/integrations-guide/)可能会在 `locals` 对象中设置属性和提供一些功能。如果你在使用一个集成，检查它的文档并确认你没有覆盖掉它的属性或者在做重复的工作。
:::

你可以在 `locals` 中存储任何类型的数据：字符串，数字，甚至复杂的数据类型，如函数和映射。

```js title="src/middleware.js"
export function onRequest ({ locals, request }, next) {
    // 拦截一个请求里的数据
    // 可选地修改 `locals` 中的属性
    locals.user.name = "John Wick";
    locals.welcomeTitle = () => {
        return "Welcome back " + locals.user.name;
    };

    // 返回一个 Response 或者调用 `next()` 的结果
    return next();
};
```

然后你可以在任何 `.astro` 文件中通过 `Astro.locals` 使用这个信息。

```astro title="src/pages/orders.astro"
---
const title = Astro.locals.welcomeTitle();
const orders = Array.from(Astro.locals.orders.entries());
---
<h1>{title}</h1>
<p>这个 {data.property} 来自中间件。</p>
<ul>
    {orders.map(order => {
        return <li>{/* do something with each order */}</li>;
    })}
</ul>
```

`locals` 是一个在单个 Astro 路由中创建和销毁的对象；当你的路由页面渲染完后，`locals` 将不再存在，并会创建一个新的。需要在多个页面请求之间保持的信息必须存储在其他地方。

:::note
`locals` 的值不能在运行时被覆盖。这样做会有清除用户存储的所有信息的风险。在 `dev` 模式下 Astro 会进行检查，如果 `locals` 被覆盖将会抛出错误。
:::

## 示例：删除敏感信息

下面的示例使用中间件将"私人信息"替换为"已删除"，以便你在页面上渲染修改后的 HTML：

```js title="src/middleware.js"
export const onRequest = async (context, next) => {
    const response = await next();
    const html = await response.text();
    const redactedHtml = html.replaceAll("私人信息", "已删除");
    
    return new Response(redactedHtml, {
        status: 200,
        headers: response.headers
    });
};
```

## 中间件类型

你可以导入并使用 `defineMiddleware()` 实用函数来提供类型安全：

```ts
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

// `context` 和 `next` 会自动被类型化
export const onRequest = defineMiddleware((context, next) => {

});
```

或者，如果你使用 JsDoc 来提供类型安全，你可以使用 `MiddlewareHandler`：

```js
// src/middleware.js
/**
 * @type {import("astro").MiddlewareHandler}
 */
// `context` 和 `next` 会自动被类型化
export const onRequest = (context, next) => {

};
```

要给 `Astro.locals` 内的信息定义类型，也就是在 `.astro` 文件和中间件代码中能提供自动补全，在 `env.d.ts` 文件中声明一个全局命名空间：

```ts title="src/env.d.ts"
/// <reference types="astro/client" />
declare namespace App {
    interface Locals {
        user: {
            name: string
        },
        welcomeTitle: () => string,
        orders: Map<string, object>
    }
}
```

然后，在中间件中，你就可以使用自动补全和类型安全了。

## 中间件链式调用

可以使用 [`sequence()`](/zh-cn/reference/api-reference/#sequence) 按指定顺序连接多个中间件：

```js title="src/middleware.js"
import { sequence } from "astro:middleware";

async function validation(_, next) {
    console.log("验证请求");
    const response = await next();
    console.log("验证响应");
    return response;
}

async function auth(_, next) {
    console.log("授权请求");
    const response = await next();
    console.log("授权响应");
    return response;
}

async function greeting(_, next) {
    console.log("问候请求");
    const response = await next();
    console.log("问候响应");
    return response;
}

export const onRequest = sequence(validation, auth, greeting);
```

控制台打印结果顺序如下：

```sh
验证请求
授权请求
问候请求
问候响应
授权响应
验证响应
```
## 错误页面

即使找不到匹配的路由，中间件也会尝试为所有按需渲染的页面运行。这包括 Astro 的默认（空白）404 页面和任何自定义 404 页面。然而，是否运行该代码取决于[适配器](/zh-cn/guides/server-side-rendering/)。一些适配器可能会提供特定平台的错误页面。

在提供 500 错误页面之前，中间件也会尝试运行，包括自定义的 500 页面，除非服务器错误发生在中间件本身的执行中。如果你的中间件没有成功运行，那么你将无法访问 `Astro.locals` 来渲染你的 500 页面。
